/*
 * Copyright 2007-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package jsr203.nio.file;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.EnumSet;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import jsr203.nio.file.spi.AbstractPath;
import jsr203.nio.file.spi.FileTypeDetector;
import jsr203.nio.file.spi.FileTypeDetector.Result.Confidence;

/**
 * Utility methods for files and directories.
 *
 * @since 1.7
 */

public final class Files {
    private Files() { }

    // file type detectors
    private static final Object lock = new Object();
    private static volatile List<FileTypeDetector> detectors = null;

    // loads all installed file type detectors
    private static List<FileTypeDetector> loadInstalledDetectors() {
        List<FileTypeDetector> list = new ArrayList<FileTypeDetector>();
        ServiceLoader<FileTypeDetector> loader = ServiceLoader
            .load(FileTypeDetector.class, ClassLoader.getSystemClassLoader());
        for (FileTypeDetector detector: loader) {
            list.add(detector);
        }
        return list;
    }

    // returns the list of installed detectors, loading them if required
    private static Iterable<FileTypeDetector> installedDetectors() {
        if (detectors == null) {
            synchronized (lock) {
                if (detectors == null) {
                    detectors = AccessController
                        .doPrivileged(new PrivilegedAction<List<FileTypeDetector>>() {
                            public List<FileTypeDetector> run() {
                                return loadInstalledDetectors();
                        }});
                }
            }
        }
        return detectors;
    }

    /**
     * Probes the content type of a file.
     *
     * <p> This method uses the installed {@link FileTypeDetector} implementations
     * to probe the given file to determine its content type. Each file type
     * detector's {@link FileTypeDetector#probeContentType probeContentType} is
     * invoked to probe the file type. When there is more than one file type
     * detector installed then the result with the highest {@link
     * Confidence confidence} is returned.
     *
     * <p> A given invocation of the Java virtual machine maintains a system-wide
     * list of file type detectors. Installed file type detectors are loaded
     * using the service-provider loading facility defined by the {@link ServiceLoader}
     * class. Installed file type detectors are loaded using the system class
     * loader. If the system class loader cannot be found then the extension class
     * loader is used; If the extension class loader cannot be found then the
     * bootstrap class loader is used. File type detectors are typically installed
     * by placing them in a JAR file on the application class path or in the
     * extension directory, the JAR file contains a provider-configuration file
     * named {@code jsr203.nio.file.spi.FileTypeDetector} in the resource directory
     * {@code META-INF/services}, and the file lists one or more fully-qualified
     * names of concrete subclass of {@code FileTypeDetector } that have a zero
     * argument constructor. If the process of locating or instantiating the
     * installed file type detectors fails then an unspecified error is thrown.
     * The ordering that installed providers are located is implementation
     * specific.
     *
     * <p> The return value of this method is the string form of the value of a
     * Multipurpose Internet Mail Extension (MIME) content type as
     * defined by <a href="http://www.ietf.org/rfc/rfc2045.txt"><i>RFC&nbsp;2045:
     * Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet
     * Message Bodies</i></a>. The string is guaranteed to be parsable according
     * to the grammar in the RFC.
     *
     * @param   file
     *          The file reference
     *
     * @return  The content type of the file, or {@code null} if the content
     *          type cannot be determined
     *
     * @throws  IOException
     *          If an I/O error occurs
     * @throws  SecurityException
     *          If a security manager is installed and it denies an unspecified
     *          permission required by a file type detector implementation.
     *
     * @see DirectoryStreamFilters#newContentTypeFilter
     */
    public static String probeContentType(FileRef file)
        throws IOException
    {
        FileTypeDetector.Result<String> result = null;

        for (FileTypeDetector detector: installedDetectors()) {
            FileTypeDetector.Result<String> res = detector.probeContentType(file);
            if (res == null) {
                continue;       // content type not recognized by this detector
            }

            // if highest confidence then we don't need to probe anymore
            if (res.getConfidence() == Confidence.HIGHEST) {
                result = res;
                break;
            }

            // if higher confidence that previous then remember it
            if ((result == null) ||
                (res.getConfidence().value() > result.getConfidence().value()))
            {
                result = res;
            }
        }

        return (result == null) ? null : result.get();
    }

    /**
     * Invokes a {@link DirectoryAction} for each entry in a directory accepted
     * by a given {@link jsr203.nio.file.DirectoryStream.Filter filter}.
     *
     * <p> This method opens the given directory and invokes the directory
     * action's {@link DirectoryAction#invoke invoke} method for each entry
     * accepted by the filter. When iteration is completed then the directory
     * is closed. If the {@link DirectoryStream#close close} method throws an
     * {@code IOException} then it is silently ignored.
     *
     * <p> Any {@code Error} or {@code RuntimeException} thrown by the {@code
     * invoke} method will be propagated by this method and will cause the
     * directory to be closed.
     *
     * @param   dir
     *          The directory
     * @param   filter
     *          The filter, or {@code null} to accept all entries
     * @param   action
     *          The {@code DirectoryAction} to invoke for each accepted entry
     *
     * @throws  NotDirectoryException
     *          If the {@code dir} parameter is not a directory <i>(optional
     *          specific exception)</i>
     * @throws  IOException
     *          If an I/O error occurs
     * @throws  SecurityException
     *          In the case of the default provider, the {@link
     *          SecurityManager#checkRead(String) checkRead} method is invoked
     *          to check read access to the directory.
     */
    public static void withDirectory(FileRef dir,
                                     DirectoryStream.Filter filter,
                                     DirectoryAction action)
        throws IOException
    {
        DirectoryStream stream = dir.newDirectoryStream(filter);
        try {
            // set to true when invoking the action so as to distinguish a
            // CME thrown by the iteration from a CME thrown by the invoke
            boolean inAction = false;
            try {
                for (DirectoryEntry entry: stream) {
                    inAction = true;
                    action.invoke(entry);
                    inAction = false;
                }
            } catch (ConcurrentModificationException cme) {
                if (!inAction) {
                    Throwable cause = cme.getCause();
                    if (cause instanceof IOException)
                        throw (IOException)cause;
                }
                throw cme;
            }

        } finally {
            try {
                stream.close();
            } catch (IOException x) { }
        }
    }

    /**
     * Invokes a {@link DirectoryAction} for each entry in a directory with a
     * file name that matches a given pattern.
     *
     * <p> This method opens the given directory and invokes the directory
     * action's {@link DirectoryAction#invoke invoke} method for each entry
     * that matches the given pattern. When iteration is completed then the
     * directory is closed. If the {@link DirectoryStream#close close} method
     * throws an {@code IOException} then it is silently ignored.
     *
     * <p> Any {@code Error} or {@code RuntimeException} thrown by the {@code
     * invoke} method will be propagated by this method and will cause the
     * directory to be closed.
     *
     * <p> The syntax and pattern language supported by this method is as
     * specified by the {@link FileSystem#getNameMatcher getNameMatcher} method.
     * The given file reference is required to be associated with a {@link
     * FileSystem}.
     *
     * <p> <b>Usage Example:</b>
     * Suppose we require to print out the names of the .java files in a
     * directory:
     * <pre>
     *     FileRef dir = ...
     *     Files.withDirectory(dir, "*.java", new DirectoryAction() {
     *         public void invoke(DirectoryEntry entry) {
     *             System.out.println(entry.getName());
     *         }
     *     });
     * </pre>
     *
     * @param   dir
     *          The directory
     * @param   syntaxAndPattern
     *          The syntax and pattern
     * @param   action
     *          The {@code DirectoryAction} to invoke for each entry
     *
     * @throws  IllegalArgumentException
     *          If the {@code dir} parameter is not a {@code FileRef} that is
     *          associated with a {@link FileSystem}
     * @throws  NotDirectoryException
     *          If the {@code dir} parameter is not a directory <i>(optional
     *          specific exception)</i>
     * @throws  IOException
     *          If an I/O error occurs
     * @throws  SecurityException
     *          In the case of the default provider, the {@link
     *          SecurityManager#checkRead(String) checkRead} method is invoked
     *          to check read access to the directory.
     */
    public static void withDirectory(FileRef dir,
                                     String syntaxAndPattern,
                                     DirectoryAction action)
        throws IOException
    {
        if (dir != null && !(dir instanceof AbstractPath))
            throw new IllegalArgumentException("'dir' is not associated with a FileSystem");
        final NameMatcher matcher = ((AbstractPath)dir).getFileSystem()
            .getNameMatcher(syntaxAndPattern);
        DirectoryStream.Filter filter = new DirectoryStream.Filter() {
            @Override
            public boolean accept(DirectoryEntry entry)  {
                return matcher.match(entry.getName());
            }
        };
        withDirectory(dir, filter, action);
    }

    /**
     * Invokes a {@link DirectoryAction} for all entries in a directory.
     *
     * <p> This method works as if invoking it were equivalent to evaluating the
     * expression:
     * <blockquote><pre>
     * withDirectory(dir, (DirectoryStream.Filter)null, action)
     * </pre></blockquote>
     *
     * @param   dir
     *          The directory
     * @param   action
     *          The {@code DirectoryAction} to invoke for each entry
     *
     * @throws  NotDirectoryException
     *          If the {@code dir} parameter is not a directory <i>(optional
     *          specific exception)</i>
     * @throws  IOException
     *          If an I/O error occurs
     * @throws  SecurityException
     *          In the case of the default provider, the {@link
     *          SecurityManager#checkRead(String) checkRead} method is invoked
     *          to check read access to the directory.
     */
    public static void withDirectory(FileRef dir, DirectoryAction action)
        throws IOException
    {
        withDirectory(dir, (DirectoryStream.Filter)null, action);
    }

    /**
     * Walks a file tree.
     *
     * <p> This method walks a file tree rooted at a given starting file. For
     * each file encountered, the given {@link FileVisitor} is invoked with a
     * reference to the file and a path relative to the starting file. File tree
     * traversal completes when all accessible files in the tree have been
     * visited, a visitor returns a result of {@link FileVisitResult#TERMINATE},
     * or an {@code Error} or {@code RuntimeException} is thrown by the visitor.
     *
     * <p> For each file encountered this method attempts to gets its {@link
     * jsr203.nio.file.spi.BasicFileAttributes}. If the attributes cannot be read
     * then the visitor's {@link FileVisitor#visitFileFailed visitFileFailed}
     * method is invoked. Otherwise, the attributes are read, and if the file is
     * not a directory, then the visitor's {@link FileVisitor#visitFile visitFile}
     * method is invoked.
     *
     * <p> Where the file is directory, it is opened as if by invoking its {@link
     * FileRef#newDirectoryStream newDirectoryStream} method. If the directory
     * cannot be opened the visitor's {@link FileVisitor#visitDirectoryFailed
     * visitDirectoryFailed} method is invoked. Otherwise, the directory is opened
     * and the visitor's {@link FileVisitor#preVisitDirectory preVisitDirectory}
     * is invoked before any of the entries in the directory are visited. If
     * an I/O error occurs then the iteration over the directory is terminated
     * and the visitor's {@code visitDirectoryFailed} method is invoked.
     * Otherwise the {@link FileVisitor#postVisitDirectory postVisitDirectory}
     * method is invoked after all entries in the directory, and all of their
     * <em>descendants</em> have been visited.
     *
     * <p> If the {@code options} parameter contains the {@link
     * FileVisitOption#DETECT_LOOPS DETECT_LOOPS} option then this method keeps
     * track of directories visited so that infinite loops can be detected. A
     * loop could arise, for example, where there is an entry in a directory that
     * is an ancestor of the directory. By default, symbolic links are not
     * automatically followed by this method. If the {@code options} parameter
     * contains the {@link FileVisitOption#FOLLOW_LINKS FOLLOW_LINKS} option then
     * symbolic links are followed and this method keeps tracks of directories
     * visited. Loop detection is done by recording the {@link
     * jsr203.nio.file.spi.BasicFileAttributes#fileKey file-key} of directories, or
     * if file keys are not available, by invoking the {@link FileRef#isSameFile
     * isSameFile} method. When following links, and the attributes of the target
     * cannot be read, then the {@code visitFile} method is invoked with the
     * attributes of the link if they can be read.
     *
     * <p> The {@code maxDepth} parameter is the maximum number of levels of
     * directories to visit. A value of {@code 0} means that only the starting
     * file is visited, unless denied by the security manager. A negative value
     * means that there isn't any maximum.
     *
     * <p> If a visitor returns a result of {@code null} then {@code
     * NullPointerException} is thrown.
     *
     * <p> When a security manager is installed and it denies access to a file,
     * then the file is ignored and the visitor is not invoked.
     *
     * @param   start
     *          The starting file
     * @param   options
     *          Options to configure the traversal
     * @param   maxDepth
     *          The maximum number of directory levels to visit, or a negative
     *          value if there is no maximum
     * @param   visitor
     *          The file visitor to invoke for each file
     *
     * @throws  SecurityException
     *          If the security manager denies access to the starting file.
     *          In the case of the default provider, the {@link
     *          SecurityManager#checkRead(String) checkRead} method is invoked
     *          to check read access to the directory.
     */
    public static void walkFileTree(FileRef start,
                                    Set<FileVisitOption> options,
                                    int maxDepth,
                                    FileVisitor visitor)
    {
        if (maxDepth < 0)
            maxDepth = FileTreeWalker.NO_MAX_DEPTH;
        new FileTreeWalker(options, visitor).walk(start, maxDepth);
    }

    /**
     * Walks a file tree.
     *
     * <p> This method works as if invoking it were equivalent to evaluating the
     * expression:
     * <blockquote><pre>
     * walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), -1, visitor)
     * </pre></blockquote>
     *
     * @param   start
     *          The starting file
     * @param   visitor
     *          The file visitor to invoke for each file
     *
     * @throws  SecurityException
     *          If the security manager denies access to the starting file.
     *          In the case of the default provider, the {@link
     *          SecurityManager#checkRead(String) checkRead} method is invoked
     *          to check read access to the directory.
     */
    public static void walkFileTree(FileRef start, FileVisitor visitor) {
        walkFileTree(start,
                     EnumSet.noneOf(FileVisitOption.class),
                     -1,
                     visitor);
    }
}
